import { errMsg } from '@zenstone/ts-utils/error';
import { isInferObj } from '@zenstone/ts-utils/object';
import { joinHttpPath, purgeHttpPath } from '@zenstone/ts-utils/path';
import { notEmptyStr } from '@zenstone/ts-utils/string';
import { getEnv } from './_env';
import { createResp, removeExtname, statusCode } from './_utils';

type PackageBasic = {
  name: string;
  version: string;
};

type NpmPackage = PackageBasic & {
  types?: string;
  typings?: string;
  exports?: Record<string, unknown>;
  dependencies?: Record<string, string>;
  devDependencies?: Record<string, string>;
};

type ParsePackage = PackageBasic & {
  path: string;
  isEmberRefs?: boolean;
};

const dftPrefix = '/pkg/';
const dftCdnBaseUrl = 'https://cdn.jsdelivr.net/npm/';
const pkgJson = 'package.json';
const typesPrefix = '@types/';
const dtsExt = '.d.ts';
const indexDts = `index${dtsExt}`;

const commonHeaders: [string, string][] = [
  ['access-control-allow-origin', '*'],
  ['access-control-expose-headers', '*'],
  ['cross-origin-resource-policy', 'cross-origin'],
  ['timing-allow-origin', '*'],
];

class PkgError extends Error {
  constructor(
    message: string,
    public readonly statusCode = 500,
  ) {
    super(message);
  }
}

const isNpmPackage = (json: unknown) =>
  isInferObj<NpmPackage>(
    json,
    (it) => notEmptyStr(it.name) && notEmptyStr(it.version),
  );

/**
 * 判断是否 pkg 前缀请求
 *
 * 保留 _req 的传入
 *
 * @param _req
 * @param url
 */
export const isPkgReq = (_req: Request, url: URL) =>
  url.pathname.startsWith(getEnv('PKG_PREFIX', dftPrefix));

/**
 * 解析 URL 请求的 pathname 为基础的 {@link ParsePackage}
 *
 * 请求输入的例子：
 *
 * - `/pkg/react/package.json`
 * - `/pkg/csstype`
 * - `/pkg/usehooks-ts@^3`
 * - `/pkg/@mui/material@^6/Button`
 *
 * 移除前缀后，对应的就是 import path
 *
 * @param _req
 * @param url
 */
const parseReqPkg = (_req: Request, url: URL) => {
  const res = parseImportPath(
    url.pathname.substring(getEnv('PKG_PREFIX', dftPrefix).length - 1),
  );
  const refs = url.searchParams.get('refs');
  if (refs !== null) {
    res.isEmberRefs = true;
  }
  return res;
};

/**
 * 解析导入路径
 *
 * @param input
 */
const parseImportPath = (input: string): ParsePackage => {
  const path = purgeHttpPath(input);
  const pattern =
    /^((?:@(?:[a-z0-9-*~][a-z0-9-*._~]*)?\/)?[a-z0-9-~][a-z0-9-._~]*)(?:@([^@^\/]+))?/gm;
  const match = pattern.exec(path);
  if (match == null) {
    throw new PkgError('Invalid package name input!', 500);
  }
  return {
    name: match[1],
    version: match[2] ? decodeURIComponent(match[2]) : '',
    path: path.substring(match[0].length + 1),
  };
};

/**
 * 基于 {@link PackageBasic} 生成对应的请求 URL
 *
 * @param name
 * @param version
 * @param path
 */
const makePkgUrl = ({ name, version }: PackageBasic, path: string) =>
  new URL(
    `${name}@${version || 'latest'}/${path}`,
    getEnv('CDN_BASE_URL', dftCdnBaseUrl),
  );

/**
 * 根据一个 {@link PackageBasic} 去获取相关的 package 信息（package.json）
 *
 * @param pkg
 */
const fetchPkgInfo = async (pkg: PackageBasic) => {
  const url = makePkgUrl(pkg, pkgJson);
  const resp = await fetch(url);
  // 如果直接用 resp.json() 可能会因为解析出错，而导致无法获取真实的错误提示文本
  const text = await resp.text();
  try {
    const json = JSON.parse(text);
    if (!isNpmPackage(json)) {
      throw new Error(`Invalid package info for ${pkg.name}@${pkg.version}`);
    }
    return json;
  } catch (err) {
    throw new PkgError(text || errMsg(err) || 'Unknown error', 404);
  }
};

/**
 * 基于 package 的 version ，推断出 types pkg 所需的版本号
 *
 * @param version
 */
const inferTypesPkgVersion = (version: string): string => {
  const match = /^([\d]+)\..*/.exec(version);
  if (match != null) {
    return `^${match[1]}`;
  }
  return version;
};

/**
 * 基于一个 {@link ParsePackage} 去生成对应的 `@types/pkgName` 的包信息
 *
 * @param pkg
 */
const makeTypesPkg = (pkg: ParsePackage): ParsePackage => {
  if (pkg.name.startsWith(typesPrefix)) {
    throw new Error('Invalid package name to generate @types package info');
  }
  return {
    name: `${typesPrefix}${pkg.name}`,
    version: inferTypesPkgVersion(pkg.version),
    path: '',
    isEmberRefs: pkg.isEmberRefs,
  };
};

const matchDepVersion = (name: string, pkg?: NpmPackage) => {
  if (pkg != null) {
    if (pkg.dependencies != null && name in pkg.dependencies)
      return pkg.dependencies[name];
    if (pkg.devDependencies != null && name in pkg.devDependencies)
      return pkg.devDependencies[name];
  }
  return '';
};

const extractExportsTypes = (obj: unknown) => {
  if (isInferObj(obj)) {
    if ('types' in obj && notEmptyStr(obj.types)) {
      return obj.types;
    }
    if ('import' in obj) {
      return extractExportsTypes(obj.import);
    }
    if ('require' in obj) {
      return extractExportsTypes(obj.require);
    }
  }
};

type SourceDep = ParsePackage & {
  url: string | URL;
  type: 'ref' | 'import' | 'export';
};

type DtsData = {
  ref: string | URL;
  path: string;
  source: string;
  deps: SourceDep[];
  typings?: boolean;
};

const createApi = (
  reqUrl: URL,
  pkg: ParsePackage,
  refPkg?: ParsePackage,
  pkgInfo?: NpmPackage,
) => {
  // 生成一个该包的 package.json 的 URL ，用于辅助生成该包其他的 URL
  // 如：new URL('./index.d.ts', pkgUrl)
  const pkgUrl = makePkgUrl(pkg, pkgJson);

  let typesApi: ReturnType<typeof createApi> | undefined;

  const dts = async (
    path: string,
    baseUrl?: string | URL,
    typings?: boolean,
  ): Promise<DtsData> => {
    const url = new URL(path, baseUrl || pkgUrl);
    const resp = await fetch(url);
    let source = await resp.text();
    if (!resp.ok) {
      throw new PkgError(source, resp.status);
    }
    source = fixSourceImportsPaths(source, baseUrl);
    let deps: SourceDep[] = [];
    if (pkg.isEmberRefs) {
      source = await attachRefs(source, url);
    } else {
      deps = deps.concat(extractRefs(source, url));
    }
    deps = deps.concat(extractImports(source, url));
    return { ref: url, path, source, deps, typings };
  };

  const dtsJson = async (
    path: string,
    baseUrl?: string | URL,
    typings?: boolean,
  ) => {
    const res = await dts(path, baseUrl, typings);
    return json.stringify({ ...(refPkg ?? pkg), ...res });
  };

  const makeUrl = (path: string, inputPkg?: PackageBasic) => {
    const _pkg = inputPkg || pkg;
    const pf = getEnv('PKG_PREFIX', dftPrefix);
    const args = [
      `${reqUrl.origin}${pf}`,
      inputPkg != null
        ? `${_pkg.name}${_pkg.version ? `@${_pkg.version}` : ''}`
        : undefined,
      path,
    ].filter(Boolean);
    const url = joinHttpPath(...args);
    return url;
  };

  const makeToken = (token: string) => `//-------(ref:${token})-------//`;

  const getRefPattern = () =>
    /^\/{3}[\s\t]+\<reference[\s\t]+path=[\'\"]([^\"\']+)[\'\"].*\/\>/gm;

  const attachRefs = async (source: string, baseUrl?: string | URL) => {
    let index = 0;
    const paths: Record<string, string> = {};
    const tokens: Record<string, string> = {};
    const temp = source.replace(getRefPattern(), (match, path) => {
      const token = `token_${index++}`;
      tokens[token] = match;
      paths[token] = path;
      return makeToken(token);
    });

    for await (const [token, path] of Object.entries(paths)) {
      const { ref, source } = await dts(path, baseUrl);
      tokens[token] = [`// ref: ${ref}`, source].join('\n');
    }

    return temp.replace(
      /\/\/-------\(ref:(token_[\d]+)\)-------\/\//gm,
      (match, token) => {
        return tokens[token] || match;
      },
    );
  };

  const extractRefs = (source: string, baseUrl?: string | URL) => {
    const matches = source.matchAll(getRefPattern());
    const deps: SourceDep[] = [];
    for (const match of matches) {
      deps.push({
        ...pkg,
        path: match[1],
        url: makeUrl(match[1], pkg),
        type: 'ref',
      });
    }
    return deps;
  };

  const getFixImportsPattern = () =>
    /^(import|export)( .* from )[\'\"]([^\"\']+)[\'\"]/gm;

  const fixSourceImportsPaths = (source: string, baseUrl?: string | URL) => {
    let reqUrlStr = reqUrl.toString();
    const pkgPf = getEnv('PKG_PREFIX', dftPrefix);
    const pkgEnd = `${pkgPf}${(refPkg || pkg).name}`;
    if (reqUrlStr.endsWith(pkgEnd)) {
      reqUrlStr += '/';
    }
    const _source = source.replace(
      getFixImportsPattern(),
      (match, method, others, path) => {
        if (path.startsWith('.')) {
          const url = new URL(path, reqUrlStr);
          let lastPath = url
            .toString()
            .replace(reqUrl.origin, '')
            .replace(pkgPf, '');
          lastPath = purgeHttpPath(lastPath);
          return `${method}${others}"${lastPath}"`;
        }
        return match;
      },
    );
    return _source;
  };

  const getImportPattern = () =>
    /^(import|export) .* from [\'\"]([^\"\']+)[\'\"]/gm;

  const extractImports = (source: string, baseUrl?: string | URL) => {
    const matches = source.matchAll(getImportPattern());
    const index: Record<string, boolean> = {};
    const deps: SourceDep[] = [];
    const url = baseUrl || pkgUrl;

    for (const match of matches) {
      const type = match[1] as SourceDep['type'];
      const path = match[2];
      if (index[path] != null) continue;

      if (path.startsWith('.')) {
        const refUrl = new URL(match[2], url);
        const relativePath = refUrl
          .toString()
          .replace(getEnv('CDN_BASE_URL', dftCdnBaseUrl), '');
        const finalUrl = makeUrl(relativePath);
        deps.push({
          ...pkg,
          path: match[2],
          url: finalUrl,
          type,
        });
      } else {
        const res = parseImportPath(path);
        if (res == null) continue;
        res.version = matchDepVersion(res.name, pkgInfo);

        deps.push({
          ...res,
          url: makeUrl(res.path, res),
          type,
        });
      }

      index[path] = true;
    }
    return deps;
  };

  return {
    pkgUrl,
    get types() {
      if (typesApi == null) {
        typesApi = createApi(reqUrl, makeTypesPkg(pkg), pkg, pkgInfo);
      }
      return typesApi;
    },
    dts,
    dtsJson,
  };
};

const { json } = createResp(commonHeaders);

/******************************************************************************
 * 主程序入口
 ******************************************************************************/
const pkgFetch = async (req: Request, url: URL): Promise<Response> => {
  const parse = parseReqPkg(req, url);

  try {
    // 不管用户输入的 version 是什么，都将去请求一次 package 信息，以确定该次请求内的明确 version
    const pkg = await fetchPkgInfo(parse);
    // 用 pkg version 写入到 parse
    parse.version = pkg.version;

    const { path } = parse;

    if (path === pkgJson) {
      return json.stringify(pkg);
    }

    // 当前包的 api
    const api = createApi(url, parse, undefined, pkg);

    if (path === '') {
      if (pkg.exports != null) {
        const types = extractExportsTypes(pkg.exports['.']);
        if (types) {
          // 如果有 esm types ，优先提取
          return await api.dtsJson(types);
        }
      }
      if (notEmptyStr(pkg.typings)) {
        return await api.dtsJson(pkg.typings, undefined, true);
      }
      if (notEmptyStr(pkg.types)) {
        return await api.dtsJson(pkg.types);
      }
      return await api.types.dtsJson(indexDts);
    }

    const basename = removeExtname(path);
    const exportPath = `./${basename}`;

    if (pkg.exports != null && pkg.exports[exportPath] != null) {
      if (!pkg.types) {
        return await api.types.dtsJson(`${basename}${dtsExt}`);
      }

      const types = extractExportsTypes(pkg.exports[exportPath]);
      if (types == null) {
        throw new Error('Cloud not extract types from package exports.');
      }
      return await api.dtsJson(types);
    }

    try {
      return await api.dtsJson(`${basename}/${indexDts}`);
    } catch (err) {
      return await api.dtsJson(`${basename}${dtsExt}`);
    }
  } catch (err) {
    return json.stringify({ ...parse, error: errMsg(err) }, statusCode(err));
  }
};

export default pkgFetch;
